#########################################################################
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    Created on 14 Feb 2012
#
#    @author: Maurizio Agelli (maurizio.agelli@gmail.com)
#
#########################################################################
import pygame
import copy
import re
from pygame.locals import FULLSCREEN, KEYDOWN, K_h, K_s, K_LEFT, K_RIGHT, K_UP, K_DOWN, K_r, K_q, K_SPACE, K_l
import datetime

R = 2.**(1/3.)
ZONE = ["I",  "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X",
        "XI", "XII", "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XIX", "XX",
        "XXI", "XXII", "XXIII", "XXIV"]

class Grayscale(object):
    '''
    List of integers representing grey levels.
    Values range from pure black(0) to pure white (255)
    '''
    def __init__(self):
        self.default_gray_levels = []
        self.gray_levels = []

    def get_levels(self):
        return self.gray_levels

    def get_zones(self):
        return self.zone

    def get_default_levels(self):
        return self.default_gray_levels

    def build(self, gamma, lv_range, shift=0, step=1.):
        '''
        Calculate the pixel values of a scale of gray levels from black (0) to white (255).

        :type  gamma:    float
        :param gamma:    the display gamma

        :type  lv_range: float
        :param lv_range: the display dynamic range, in stops

        :type  shift:    integer
        :param shift:    over-exposure shift in stops (allows to extend the grayscale beyond its dynamic by over-exposing a second shot)

        :type  step:     float
        :param step:     the brightness difference between two adjacent gray levels, in stops.
                         Default value is 1.
        '''
        def delta(x):
            return (R*x**self.gamma + (R-1.)/(self.contrast-1.))**(1./self.gamma) - x
        self.gamma = gamma
        self.step = step
        self.contrast = 2. ** lv_range
        self.gray_levels = []
        x = 0.
        j = 0
        while int(x*255.+.5) <= 255.:
            self.gray_levels.append(int(x*255. + .5))
            j += 1
            x1 = delta(x) + x
            x2 = delta(x1) + x1
            x = delta(x2) + x2
        self.zone = ZONE[shift:j+shift]
        last = self.zone[-1]
        if int(x1*255.+.5) <=255.:
            self.gray_levels.append(int(x1*255.+.5))
            self.zone.append(last+'+1/3')
        if int(x2*255.+.5) <=255.:
            self.gray_levels.append(int(x2*255.+.5))
            self.zone.append(last+'+2/3')
        self.default_gray_levels = copy.copy(self.gray_levels)

    def change_brightness(self, index, inc):
        '''
        Modify the gray level at position <index> by a value <inc>.
        '''
        self.gray_levels[index] = max(0, min(255, self.gray_levels[index] + inc))

    def reset(self):
        '''
        Reset all gray levels which have been modified by the change_brightness() method.
        '''
        self.gray_levels[0:len(self.gray_levels)] = self.default_gray_levels[0:len(self.gray_levels)]

    def save(self):
        '''
        Save settings and gray levels to a file
        '''
        pass

    def load(self):
        '''
        Load settings and gray levels from file
        '''
        pass

class GC(object):
    '''
    graphics context
    '''
    def __init__(self):
        pygame.display.init()
        self.screen_size = self.screen_w, self.screen_h = pygame.display.Info().current_w, pygame.display.Info().current_h
        self.screen = pygame.display.set_mode(self.screen_size, FULLSCREEN)
        pygame.font.init()
        pygame.mouse.set_visible(False)
        pygame.key.set_repeat(600, 100)
        self.font10 = pygame.font.Font(None, 10)
        self.font12 = pygame.font.Font(None, 12)
        self.font14 = pygame.font.Font(None, 14)
        self.font16 = pygame.font.Font(None, 16)
        self.font32 = pygame.font.Font(None, 32)

class Parameter(object):
    def __init__(self, choices, values, default, current, position):
        self.choices = choices
        self.values = values
        self.default = default
        self.current = current
        self.position = position

class Layer(object):
    def __init__(self, gc, x, y, w, h, color=None, active = False):
        '''
        Create a window layer at screen coordinate (x, y) with dimensions (w, h)
        '''
        self.gc = gc
        self.active = active
        self.x_origin = x
        self.y_origin = y
        self.width = w
        self.height = h
        self.color = color
        if color is not None:
            self.clear()

    def show(self):
        self.active = True

    def hide(self):
        self.active = False

    def toggle(self):
        self.active = not self.active

    def is_active(self):
        return self.active

    def clear(self):
        if self.active:
            pygame.draw.rect(self.gc.screen, self.color, (self.x_origin, self.y_origin, self.width, self.height))

    def write_text(self, x, y, msg, fg_col, font_size, align='left'):
        '''
        write some text at relative coordinates (x,y)
        '''
        font = pygame.font.Font(None, font_size)
        text = font.render(msg, True, fg_col)
        textRect = text.get_rect()
        if align=='left':
            textRect.centerx = self.x_origin + x + textRect.w / 2
        else: # center alignement
            textRect.centerx = self.x_origin + x
        textRect.centery = self.y_origin + y + textRect.h / 2
        textRect.centery = self.y_origin + y
        self.gc.screen.blit(text, textRect)

class CursorLayer(Layer):
    '''
    This class can manage rectangular cursors which can be moved horizontally.
    '''
    def __init__(self, gc, y, width, height, color, num_positions, x_start, x_step, position):
        '''
        Create a rectangular cursor which can assume <num_positions> positions on an horizontal line, from (x_start,y) to (x_end,y)
        The created cursor is made active in position 1
        Argument x is ignored.
        '''
        Layer.__init__(self, gc, 0, y, width, height, color, True)
        self.x_start = x_start
        self.x_step = x_step
        self.set_position(position, num_positions)

    def set_position(self, pos, num_positions=None):
        '''
        Change the horizontal position of the cursor. If the <num_positions> argument is provided,
        recalculates the available positions.
        '''
        if num_positions is not None:
            self.num_positions = num_positions
        self.position = pos
        if self.position >= self.num_positions:
            self.position = 0
        elif self.position < 0:
            self.position = self.num_positions - 1
        #self.step = (self.x_end - self.x_start) / self.num_positions
        self.min_x = self.x_start + self.x_step / 2.
        self.x_origin = self.min_x - self.width/2. + self.x_step * self.position
        return self.position

    def get_position(self):
        return self.position

    def move(self, inc):
        return self.set_position(self.position + inc)

    def draw(self):
        '''
        Draw cursor layer onto screen
        '''
        self.clear()

class SettingsLayer(Layer):
    '''
    Build a bar at the bottom of the screen showing a set of configurable parameters.
    Provide methods to retrieve and modify parameters.
    '''
    def __init__(self, gc, x, y, width, height, x_start, x_end, bg_color, caption_color, value_color, value_changed_color, font_size):
        Layer.__init__(self, gc, x, y, width, height, bg_color, False)
        self.w = width
        self.param = {}
        self.active= False
        self.font_size = font_size
        self.param_names = []
        self.x_start = x_start
        self.x_end = x_end
        self.value_color = value_color
        self.value_changed_color = value_changed_color

    def add_param(self, position, name, choices, default, values):
        '''
        Configure the settings bar to show a parameter in a given position.
        '''
        self.param[name] =  Parameter(choices, values, default, default, position)
        self.param_names.append(name)

    def get(self, param_name):
        return self.param[param_name].values[self.param[param_name].current]

    def set_param_value(self, position, inc):
        self.param[self.param_names[position]].current = min(max(0, self.param[self.param_names[position]].current + inc),
                                                             len(self.param[self.param_names[position]].choices)-1)
    def reset(self):
        for i in self.param.keys():
            self.param[i].current = self.param[i].default

    def get_num_params(self):
        return len(self.param)

    def draw(self):
        '''
        Draw the setting bar on the screen.
        '''
        if self.active:
            delta = (self.x_end - self.x_start) / len(self.param)
            self.clear()
            x = self.x_start
            for p in self.param_names:
                if self.param[p].choices[self.param[p].current] == self.param[p].choices[self.param[p].default]:
                    col = self.value_color
                else:
                    col = self.value_changed_color
                self.write_text(x, self.height/2, p + ' = ' + self.param[p].choices[self.param[p].current], col, self.font_size, 'left')
                x += delta

class GrayBarsLayer(Layer):
    '''
    This class provides several methods for drawing and modifying a set of gray bars on the background of the screen
    '''
    def __init__(self, gc, gray_levels, zones, default_gray_levels):
        Layer.__init__(self, gc, 0, 0, gc.screen_w, gc.screen_h, None, True)
        self.font = gc.font16
        self.cursor_on = self.settings_bar_active = False
        self.gray_levels = gray_levels
        self.default_gray_levels = default_gray_levels
        self.zones = zones

    def draw(self, gray_levels, index=0, separators=False):
        # redraw the grayscale (or the single full-screen zone)
        step = (self.gc.screen_w + 1)/ len(gray_levels)
        offset = 0
        for pixval in gray_levels:
            visible_pixval = 250-250*(pixval>128)
            pygame.draw.rect(self.gc.screen, (pixval, pixval, pixval), (offset, 0, offset + step - 0, self.gc.screen_h)) # gray bars
            if offset and separators:
                pygame.draw.line(self.gc.screen, (128,128,128), (offset, 0), (offset, self.gc.screen_h), 2)
            self.write_text(offset + (step - 1) / 2., 19, self.zones[index], (visible_pixval, visible_pixval, visible_pixval), 48, 'center')
            if self.gray_levels[index] == self.default_gray_levels[index]:  # write pixel values
                color = (visible_pixval, visible_pixval, visible_pixval)
            else:
                color = (250, visible_pixval/4, visible_pixval/4)
            self.write_text(offset + (step - 1) / 2., 45, str(self.gray_levels[index]), color, 16, 'center')
            offset += step
            index += 1

def main():
    gc = GC()
    x_start = 10
    x_end=gc.screen_w*4/5
    settings_bar = SettingsLayer(gc, x=0, y=gc.screen_h-40, width=gc.screen_w, height=40, x_start=x_start, x_end=x_end,
                                 bg_color=(200, 200, 220), caption_color=(111, 122, 233),
                                 value_color=(20, 20, 20), value_changed_color=(70, 7, 5), font_size=32)
    settings_bar.add_param(position=0,
                           name='gamma',
                           choices=['1.8', '2.2'],
                           default=1,
                           values=[ 1.8 , 2.2 ])
    settings_bar.add_param(position=1,
                           name='lv_min',
                           choices=['-2', '-(1 + 2/3)','-(1 + 1/3)', '-1', '-2/3', '-1/3', '0', '1/3', '2/3', '1'],
                           default=7,
                           values=[ -2., -1.6667     , -1.3333    ,  -1.,-0.6667,-0.3333,  0 ,0.3333,0.6667,  1.])
    settings_bar.add_param(position=2,
                           name='lv_max',
                           choices=['8', '8 + 1/3','8 + 2/3', '9', '9 + 1/3', '9 + 2/3', '10', '10 + 1/3', '10 + 2/3', '11'],
                           default=3,
                           values=[ 8. , 8.3333  , 8.6667,    9.,  9.3333   , 9.6667  ,  10.,  10.3333  ,  10.6667,    11.])
    settings_bar.add_param(position=3,
                           name='iso',
                           choices=['25','32','40','50','64','80','100','125','160','200','250','320','400','500','640','800'],
                           default=6,
                           values=[])
    choices = ['Acros 100','FP4+','HP5+','TMAX 100','TMAX 400','TRI-X','PAN F','Kentmere 100','Kentmere 400',
               'Efke 25','Efke 50','Fomapan 100','Fomapan 400','Delta 100','Delta 400','SFX 200']
    choices.sort()
    settings_bar.add_param(position=4,
                           name='film',
                           choices=choices,
                           default=6,
                           values=[])
    settings_bar.add_param(position=5,
                           name='over_exposure',
                           choices=['0','+1','+2','+3','+4','+5', '+6', '+7', '+8', '+9', '+10'],
                           default=0,
                           values = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    cursor2_position = 0
    cursor2 = CursorLayer(gc, y=gc.screen_h-14, width=70, height=4,
                          color=(10, 100, 10),  num_positions=settings_bar.get_num_params(),
                          x_start=x_start, x_step = (x_end - x_start)/settings_bar.get_num_params(), position=cursor2_position)
    grayscale = Grayscale()
    help_window = Layer(gc, gc.screen_w/4, gc.screen_h/4, gc.screen_w/2, gc.screen_h/2, (200, 210, 230), False)
    f = open('help.txt', 'r')
    help_text = f.readlines()
    f.close()
    full_screen = done = False
    rebuild_required = True
    cursor1_position = 1
    separators = False
    while not done:
        #####################################
        #  rebuild if settings have changed
        #####################################
        if rebuild_required:
            grayscale.build(settings_bar.get('gamma'), settings_bar.get('lv_max') - settings_bar.get('lv_min'), settings_bar.get('over_exposure'))
            gray_bars = GrayBarsLayer(gc, grayscale.get_levels(), grayscale.get_zones(), grayscale.get_default_levels())
            cursor1 = CursorLayer(gc, y=55, width=24, height=6, color=(128, 30, 30), num_positions=len(grayscale.get_levels()),
                          x_start=0, x_step=gc.screen_w/len(grayscale.get_levels()), position=cursor1_position)
            rebuild_required = False
            redraw_required = True
        ###########################
        #  redraw all layers
        ###########################
        if redraw_required:
            if settings_bar.is_active():
                cursor1.hide()
                cursor2.show()
            else:
                cursor1.show()
                cursor2.hide()
            if full_screen:
                gray_bars.draw(grayscale.get_levels()[cursor1.get_position():cursor1.get_position()+1], cursor1.get_position(), False)
                cursor1.hide()
            else:
                gray_bars.draw(grayscale.get_levels(), 0, separators)
            cursor1.draw()
            settings_bar.draw()
            if False and settings_bar.is_active():
                settings_bar.write_text(gc.screen_w*.9, 12, datetime.datetime.now().strftime("%Y-%m-%d %H:%M"), (9,9,9), 28, 'center')
                dynamic = settings_bar.param['lv_max'].values[settings_bar.param['lv_max'].current] - settings_bar.param['lv_min'].values[settings_bar.param['lv_min'].current]
                settings_bar.write_text(90, settings_bar.height/2, 'dynamic = ' + str(dynamic) + ' stops', settings_bar.value_color, 20, 'center')
            cursor2.draw()
            if help_window.is_active():
                help_window.clear()
                y = 0
                for t in help_text:
                    y += 25
                    help_window.write_text(10, y, re.sub('\n','',t), (11,11,11), 28, 'left')
            pygame.display.update()
            redraw_required = False
        ###########################
        #  manage keyboard events
        ###########################
        for event in pygame.event.get():
            if (event.type == KEYDOWN):
                redraw_required = True
                if (event.key == K_h):
                    help_window.toggle()
                elif (event.key == K_s):
                    settings_bar.toggle()
                elif (event.key == K_RIGHT and settings_bar.is_active()):
                    cursor2_position = cursor2.move(1)
                elif (event.key == K_RIGHT and not settings_bar.is_active()):
                    cursor1_position = cursor1.move(1)
                elif (event.key == K_LEFT and settings_bar.is_active()):
                    cursor2_position = cursor2.move(-1)
                elif (event.key == K_LEFT and not settings_bar.is_active()):
                    cursor1_position = cursor1.move(-1)
                elif (event.key == K_UP and not settings_bar.is_active() and cursor1.get_position()<>0 and
                      cursor1.get_position() <> (len(grayscale.get_levels()) - 1)):
                    grayscale.change_brightness(cursor1.get_position(), 1)
                elif (event.key == K_DOWN and not settings_bar.is_active() and cursor1.get_position()<>0 and
                      cursor1.get_position() <> (len(grayscale.get_levels()) - 1)):
                    grayscale.change_brightness(cursor1.get_position(), -1)
                elif (event.key == K_UP and settings_bar.is_active()):
                    settings_bar.set_param_value(cursor2.get_position(), 1)
                    if cursor2.get_position() < 3:
                        rebuild_required = True
                elif (event.key == K_DOWN and settings_bar.is_active()):
                    settings_bar.set_param_value(cursor2.get_position(), -1)
                    if cursor2.get_position() < 3:
                        rebuild_required = True
                elif (event.key == K_SPACE):
                    full_screen = not full_screen
                elif (event.key == K_l):
                    separators = not separators
                elif (event.key == K_r):
                    settings_bar.reset()
                    grayscale.reset()
                    rebuild_required = True
                elif (event.key == K_q):  # quit
                    done = True
    pygame.quit()

if __name__ == "__main__":
    main()
